/*
 * Copyright (C) 2014 Jörg Prante
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.xbib.io.compress.lzf;

import java.io.IOException;
import java.io.InputStream;

/**
 * Safe {@link ChunkDecoder} implementation that can be used on any platform.
 */
public class VanillaChunkDecoder extends ChunkDecoder {

    public VanillaChunkDecoder() {
    }

    @Override
    public final int decodeChunk(final InputStream is, final byte[] inputBuffer, final byte[] outputBuffer)
            throws IOException {
        int bytesInOutput;
        /* note: we do NOT read more than 5 bytes because otherwise might need to shuffle bytes
         * for output buffer (could perhaps optimize in future?)
         */
        int bytesRead = readHeader(is, inputBuffer);
        if ((bytesRead < HEADER_BYTES)
                || inputBuffer[0] != LZFChunk.BYTE_Z || inputBuffer[1] != LZFChunk.BYTE_V) {
            if (bytesRead == 0) { // probably fine, clean EOF
                return -1;
            }
            throw new IOException("Corrupt input data, block did not start with 2 byte signature ('ZV') followed by type byte, 2-byte length)");
        }
        int type = inputBuffer[2];
        int compLen = uint16(inputBuffer, 3);
        if (type == LZFChunk.BLOCK_TYPE_NON_COMPRESSED) { // uncompressed
            readFully(is, false, outputBuffer, 0, compLen);
            bytesInOutput = compLen;
        } else { // compressed
            readFully(is, true, inputBuffer, 0, 2 + compLen); // first 2 bytes are uncompressed length
            int uncompLen = uint16(inputBuffer, 0);
            decodeChunk(inputBuffer, 2, outputBuffer, 0, uncompLen);
            bytesInOutput = uncompLen;
        }
        return bytesInOutput;
    }

    @Override
    public final void decodeChunk(byte[] in, int inPos, byte[] out, int outPos, int outEnd)
            throws IOException {
        do {
            int ctrl = in[inPos++] & 255;
            if (ctrl < LZFChunk.MAX_LITERAL) { // literal run
                switch (ctrl) {
                    case 31:
                        out[outPos++] = in[inPos++];
                    case 30:
                        out[outPos++] = in[inPos++];
                    case 29:
                        out[outPos++] = in[inPos++];
                    case 28:
                        out[outPos++] = in[inPos++];
                    case 27:
                        out[outPos++] = in[inPos++];
                    case 26:
                        out[outPos++] = in[inPos++];
                    case 25:
                        out[outPos++] = in[inPos++];
                    case 24:
                        out[outPos++] = in[inPos++];
                    case 23:
                        out[outPos++] = in[inPos++];
                    case 22:
                        out[outPos++] = in[inPos++];
                    case 21:
                        out[outPos++] = in[inPos++];
                    case 20:
                        out[outPos++] = in[inPos++];
                    case 19:
                        out[outPos++] = in[inPos++];
                    case 18:
                        out[outPos++] = in[inPos++];
                    case 17:
                        out[outPos++] = in[inPos++];
                    case 16:
                        out[outPos++] = in[inPos++];
                    case 15:
                        out[outPos++] = in[inPos++];
                    case 14:
                        out[outPos++] = in[inPos++];
                    case 13:
                        out[outPos++] = in[inPos++];
                    case 12:
                        out[outPos++] = in[inPos++];
                    case 11:
                        out[outPos++] = in[inPos++];
                    case 10:
                        out[outPos++] = in[inPos++];
                    case 9:
                        out[outPos++] = in[inPos++];
                    case 8:
                        out[outPos++] = in[inPos++];
                    case 7:
                        out[outPos++] = in[inPos++];
                    case 6:
                        out[outPos++] = in[inPos++];
                    case 5:
                        out[outPos++] = in[inPos++];
                    case 4:
                        out[outPos++] = in[inPos++];
                    case 3:
                        out[outPos++] = in[inPos++];
                    case 2:
                        out[outPos++] = in[inPos++];
                    case 1:
                        out[outPos++] = in[inPos++];
                    case 0:
                        out[outPos++] = in[inPos++];
                }
                continue;
            }
            // back reference
            int len = ctrl >> 5;
            ctrl = -((ctrl & 0x1f) << 8) - 1;
            if (len < 7) { // 2 bytes; length of 3 - 8 bytes
                ctrl -= in[inPos++] & 255;
                out[outPos] = out[outPos++ + ctrl];
                out[outPos] = out[outPos++ + ctrl];
                switch (len) {
                    case 6:
                        out[outPos] = out[outPos++ + ctrl];
                    case 5:
                        out[outPos] = out[outPos++ + ctrl];
                    case 4:
                        out[outPos] = out[outPos++ + ctrl];
                    case 3:
                        out[outPos] = out[outPos++ + ctrl];
                    case 2:
                        out[outPos] = out[outPos++ + ctrl];
                    case 1:
                        out[outPos] = out[outPos++ + ctrl];
                }
                continue;
            }

            // long version (3 bytes, length of up to 264 bytes)
            len = in[inPos++] & 255;
            ctrl -= in[inPos++] & 255;

            // First: if there is no overlap, can just use arraycopy:
            if ((ctrl + len) < -9) {
                len += 9;
                if (len <= 32) {
                    copyUpTo32WithSwitch(out, outPos + ctrl, out, outPos, len - 1);
                } else {
                    System.arraycopy(out, outPos + ctrl, out, outPos, len);
                }
                outPos += len;
                continue;
            }

            // otherwise manual copy: so first just copy 9 bytes we know are needed
            out[outPos] = out[outPos++ + ctrl];
            out[outPos] = out[outPos++ + ctrl];
            out[outPos] = out[outPos++ + ctrl];
            out[outPos] = out[outPos++ + ctrl];
            out[outPos] = out[outPos++ + ctrl];
            out[outPos] = out[outPos++ + ctrl];
            out[outPos] = out[outPos++ + ctrl];
            out[outPos] = out[outPos++ + ctrl];
            out[outPos] = out[outPos++ + ctrl];

            // then loop
            // Odd: after extensive profiling, looks like magic number
            // for unrolling is 4: with 8 performance is worse (even
            // bit less than with no unrolling).
            len += outPos;
            final int end = len - 3;
            while (outPos < end) {
                out[outPos] = out[outPos++ + ctrl];
                out[outPos] = out[outPos++ + ctrl];
                out[outPos] = out[outPos++ + ctrl];
                out[outPos] = out[outPos++ + ctrl];
            }
            switch (len - outPos) {
                case 3:
                    out[outPos] = out[outPos++ + ctrl];
                case 2:
                    out[outPos] = out[outPos++ + ctrl];
                case 1:
                    out[outPos] = out[outPos++ + ctrl];
            }
        } while (outPos < outEnd);

        // sanity check to guard against corrupt data:
        if (outPos != outEnd) {
            throw new IOException("Corrupt data: overrun in decompress, input offset " + inPos + ", output offset " + outPos);
        }
    }

    protected static void copyUpTo32WithSwitch(byte[] in, int inPos, byte[] out, int outPos,
                                               int lengthMinusOne) {
        switch (lengthMinusOne) {
            case 31:
                out[outPos++] = in[inPos++];
            case 30:
                out[outPos++] = in[inPos++];
            case 29:
                out[outPos++] = in[inPos++];
            case 28:
                out[outPos++] = in[inPos++];
            case 27:
                out[outPos++] = in[inPos++];
            case 26:
                out[outPos++] = in[inPos++];
            case 25:
                out[outPos++] = in[inPos++];
            case 24:
                out[outPos++] = in[inPos++];
            case 23:
                out[outPos++] = in[inPos++];
            case 22:
                out[outPos++] = in[inPos++];
            case 21:
                out[outPos++] = in[inPos++];
            case 20:
                out[outPos++] = in[inPos++];
            case 19:
                out[outPos++] = in[inPos++];
            case 18:
                out[outPos++] = in[inPos++];
            case 17:
                out[outPos++] = in[inPos++];
            case 16:
                out[outPos++] = in[inPos++];
            case 15:
                out[outPos++] = in[inPos++];
            case 14:
                out[outPos++] = in[inPos++];
            case 13:
                out[outPos++] = in[inPos++];
            case 12:
                out[outPos++] = in[inPos++];
            case 11:
                out[outPos++] = in[inPos++];
            case 10:
                out[outPos++] = in[inPos++];
            case 9:
                out[outPos++] = in[inPos++];
            case 8:
                out[outPos++] = in[inPos++];
            case 7:
                out[outPos++] = in[inPos++];
            case 6:
                out[outPos++] = in[inPos++];
            case 5:
                out[outPos++] = in[inPos++];
            case 4:
                out[outPos++] = in[inPos++];
            case 3:
                out[outPos++] = in[inPos++];
            case 2:
                out[outPos++] = in[inPos++];
            case 1:
                out[outPos++] = in[inPos++];
            case 0:
                out[outPos++] = in[inPos++];
        }
    }
}
